/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.connection.parser.json;

import com.google.gjson.JSONArray;
import com.google.gjson.JSONBoolean;
import com.google.gjson.JSONException;
import com.google.gjson.JSONNumber;
import com.google.gjson.JSONObject;
import com.google.gjson.JSONParser;
import com.google.gjson.JSONString;
import com.google.gjson.JSONValue;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.connection.parser.CommandParsingException;
import com.hercules.android.receiver.connection.parser.EventEncodingException;
import com.hercules.android.receiver.connection.parser.MalformedCommandException;
import com.hercules.android.receiver.connection.parser.MessageParser;
import com.hercules.android.receiver.model.commands.XRECommand;
import com.hercules.android.receiver.model.events.XREEvent;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;

/**
 * <p>
 * As the sole implementation of MessageParser in this component, this class can parse the given command as a JSON based
 * string into the XRECommand and encode the event represented by XREEvent into a string with JSON format.
 * </p>
 * <p>
 * AndroidAssembly2 this class is switched to new type of parsing
 * <p>
 * Thread safety: This class is mutable, no synchronization performed for accessing the mutable field, hence it's thread
 * unsafe.
 * </p>
 * <p>
 * API Usage :<br>
 * 1. Create the JSON message parser. <br>
 * 2. Parse commandsString by calling parseCommands(String) method. The return value is the result. <br>
 * 3. Encode the event represented by XREEvent into a string with JSON format by calling encodeEvent(XREEvent) methods.
 * The return value is the result. <br>
 * <br>
 * // Create the JSON message parser with no logging performed.<br>
 * JSONMessageParser parser = new JSONMessageParser();<br>
 * Logger logger = Logger.getLogger("Logger");<br>
 * Logger anotherLogger = Logger.getLogger("anotherLogger");<br>
 * // Or create it with logger specified.<br>
 * parser = new JSONMessageParser(logger);<br>
 * // Get the logger<br>
 * logger = parser.getLogger();<br>
 * // Set the logger<br>
 * parser.setLogger(anotherLogger);<br>
 * // Suppose commandsString's contents is "[{"command": "NEW", "id": 2051, "klass": "XREView", "params": {
 * "dimensions": [600, 600] }, "commandIndex": 56, "timestamp": 12393187203234}, {"command" : "SHUTDOWN",
 * "commandIndex": 57, "timestamp":32134358943141}], then two instances are contained in the generated commands list.
 * The first one is an instance of XRENewCommand, with id set to 2051, klass set to "XREView", params map has a single
 * element with "dimensions" as the key and an int array containing two 600 as the value, commandIndex set to 56,
 * timestamp set to 12393187203234. The second one is an instance of XREShutdownCommand , with commandIndex set to 57
 * and timestamp set to 32134358943141.<br>
 * String commandsString = "[{\"command\": \"NEW\"" + ", \"id\": 2051, \"klass\": \"XREView\" ,
 * \"params\": { \"dimensions\": [600, 600] }" + " , \"commandIndex\": 56, \"timestamp\": 12393187203234}, {\"command\"
 * : \"SHUTDOWN\"<br>
 * , \"commandIndex\": 57"+ ", \"timestamp\":32134358943141}]";<br>
 * // Parse the given commands as a JSON based string into a list of XRECommands.<br>
 * List < XRECommand > commands = parser.parseCommands(commandsString);<br>
 * // And, if the commandsString's contents is only "{"command": "SHUTDOWN", "commandIndex" : 57,
 * "timestamp":32134358943141}", then only the shutdown command is created and contained in the result list.<br>
 * commandsString = "{\"command\": \"SHUTDOWN\", \"commandIndex\": 57, \"timestamp\":32134358943141}";<br>
 * // Parse the given commands as a JSON based string into a list of XRECommands.<br>
 * commands = parser.parseCommands(commandsString);<br>
 * // Suppose the event passed in is an XRECommandSequenceEvent instance with name property "onCommandSequenceExecuted",
 * source property1234, handler property 2345, eventIndex property 14 and timestamp property 12393187208356 , The string
 * "{"name": " onCommandSequenceExecuted ", "source": 1234, "handler": 2345, "eventIndex": 14 , "timestamp":
 * 12393187208356}" will be returned to the caller.<br>
 * XRECommandSequenceEvent event = new XRECommandSequenceEvent();<br>
 * event.setName(" onCommandSequenceExecuted ");<br>
 * event.setSource(1234);<br>
 * event.setHandler(2345);<br>
 * event.setEventIndex(14);<br>
 * event.setTimestamp(12393187208356.0);<br>
 * // Encode the event represented by XREEvent into a string with JSON format of the messages given in the XRE
 * Specification.<br>
 * String encodedEvent = parser.encodeEvent(event); <br>
 * </p>
 *
 * @author nhzp339, wz12
 * @author 7realm
 * @version 1.0
 */
@SuppressWarnings("unchecked")
public class JSONMessageParser implements MessageParser {
    /**
     * <p>
     * This is the logger used in this message parser. It will log the error and debug (method entry/exit) info for the
     * parser. Logging is optional, when logger is null no logging will be performed. The detailed logging mechanism can
     * be found in C.S. section 1.3.1.
     * </p>
     * <p>
     * This field is initialized in the constructor, can be modified via the setter and gotten via the getter. It can be
     * null which means logging will not be performed. Every method will use it to perform logging.
     * </p>
     */
    private Logger logger = LogUtil.getLogger(getClass().getSimpleName());

    /**
     * <p>
     * The default constructor. It does nothing and leave the logger field null.
     * </p>
     */
    public JSONMessageParser() {
        // empty
    }

    /**
     * <p>
     * The constructor which will initialize the logger used.
     * </p>
     *
     * @param logger The logger used in this message parser. Can be null.
     */
    public JSONMessageParser(Logger logger) {
        this.logger = logger;
    }

    /**
     * <p>
     * The getter for the logger. Just return the logger field.
     * </p>
     *
     * @return The logger used in this message parser. May be null.
     */
    public Logger getLogger() {
        return logger;
    }

    /**
     * <p>
     * The setter for the logger. Just assign the logger field with the argument passed in.
     * </p>
     *
     * @param logger The logger used in this message parser. Can be null.
     */
    public void setLogger(Logger logger) {
        this.logger = logger;
    }

    /**
     * <p>
     * This method parses the given commands as a JSON based string into a list of XRECommands.
     * </p>
     *
     * @param commands The JSON based string representation of the commands. Can't be null or empty. If it's a single
     * command, it's formatted as "{command}" or "[ {command} ]", if it's multiple commands, it's formatted as
     * "[ {command}, {command},...... {command} ]".
     * @return The XRECommands parsed from the string representation of the commands. Won't be null and won't be empty.
     * @throws IllegalArgumentException If command argument is null or empty.
     * @throws MalformedCommandException If the commands string's contents can't be recognized by the message parser.
     * @throws CommandParsingException If other error occurs during the parsing process of commands' string
     * representations to XRECommand.
     */
    public List<XRECommand> parseCommands(String commands) throws CommandParsingException {
        final String signature = "parseCommands";
        LogUtil.enter(logger, this, signature, commands);

        checkStringNotNullAndEmpty(signature, commands, "commands");

        // we should trim the string first
        commands = commands.trim();

        ItemParser commandParser = new CommandParser(true);
        List<XRECommand> commandList = new ArrayList<XRECommand>();
        try {
        	JSONValue parseResult = JSONParser.parse(commands);
        	
        	Object command = commandParser.parse(new StringBuilder(64), parseResult.isObject(), null, null, null);
            commandList.add((XRECommand) command);
        } catch (JSONException e) {
            // AndroidAssembly1 exception message corrected
            throw LogUtil.error(logger, this, signature, new MalformedCommandException(
                "Get JSONException during the parsing process " + " of commands string representations '" + commands
                    + "' to XRECommand.", e));
        }

        return LogUtil.exit(logger, this, signature, commandList);
    }

    /**
     * <p>
     * This method encodes the event represented by XREEvent into a string with JSON format of the messages given in the
     * XRE Specification.
     * </p>
     *
     * @param event The event represented by XREEvent. Can't be null.
     * @return The JSON format string representation of the event. Won't be null and won't be empty.
     * @throws IllegalArgumentException If event argument is null.
     * @throws EventEncodingException If error occurs during the encoding process of XREEvent into the JSON format
     * String representation.
     */
    public String encodeEvent(XREEvent event) throws EventEncodingException {
        final String signature = "encodeEvent";
        LogUtil.enter(logger, this, signature, event);

        checkNotNull(signature, event, "event");

        // get JSONObject from given XREEvent
        String result;
        try {
            JSONObject object = getJSONObjectByXREEvent(event);
            result = object.toString();
        } catch (JSONException e) {
            throw new EventEncodingException("JSON Error occurred while getting JSON object from XREEvent '" + event
                + "'.", e);
        }

        return LogUtil.exit(logger, this, signature, result);
    }

    /**
     * <p>
     * This method help to get a JSON object by an XREEvent.
     * </p>
     *
     * @param object The object instance.
     * @return The JSONObject instance.
     * @throws JSONException if error occurred while converting given object to JSON
     */
    private JSONObject getJSONObjectByXREEvent(Object object) throws JSONException {
        JSONObject jsonObject = new JSONObject();
        Class<?> clazz = object.getClass();
        while (clazz != Object.class) {
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                // AndroidAssembly1 skip constants
                if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
                    continue;
                }

                try {
                    // get field value
                    field.setAccessible(true);
                    Object fieldValue = field.get(object);

                    jsonObject.put(field.getName(), getJSONValue(fieldValue));
                } catch (IllegalAccessException e) {
                    // it will never occur because we set accessible to true
                }
            }
            // process the parent class
            clazz = clazz.getSuperclass();
        }
        return jsonObject;
    }

    /**
     * Gets JSON value form object.
     *
     * @param value the object to convert to JSON
     * @return the converted JSON object
     * @throws JSONException if error occurred while converting object
     */
    private JSONValue getJSONValue(Object value) throws JSONException {
        if (value == null) {
            return null;
        }
        // primitive class
        Class<?> valueClass = value.getClass();
        if (value instanceof String) {
        	return new JSONString((String) value);
        } else if (value instanceof Integer) {
        	return new JSONNumber(((Number) value).doubleValue());
        } else if (value instanceof Double ||value instanceof Float) {
        	return new JSONNumber(((Number) value).doubleValue());
        } else if (value instanceof Boolean) {
        	return JSONBoolean.getInstance((Boolean) value);
        } else if (valueClass.isArray()) {
        	JSONArray array = new JSONArray();

            for (int i = 0; i < Array.getLength(value); i++) {
                array.add(getJSONValue(Array.get(value, i)));
            }

            return array;
        } else if (value instanceof List) {
        	List<?> listValue = (List<?>) value;
        	JSONArray array = new JSONArray();

            for (Object elem : listValue) {
                array.add(getJSONValue(elem));
            }

            return array;
        } else if (value instanceof Map) {
            JSONObject json = new JSONObject();

            Map<String, ?> map = (Map<String, ?>) value;
            for (Entry<String, ?> entry : map.entrySet()) {
                json.put(entry.getKey(), getJSONValue(entry.getValue()));
            }

            return json;
        } else if (valueClass.isEnum()) {
        	return new JSONString(value.toString());
        } else if (value instanceof JSONValue) {
        	return (JSONValue) value;
        }

        // process as XREEvent
        return getJSONObjectByXREEvent(value);
    }

    /**
     * <p>
     * Throw IllegalArgumentException if the parameter is null.
     * </p>
     *
     * @param signature The signature of the method to be logged.
     * @param param The argument to be checked.
     * @param name The name of the argument showed in exception.
     * @throws IllegalArgumentException If the argument does not meet the request.
     */
    private void checkNotNull(String signature, Object param, String name) {
        if (param == null) {
            throw LogUtil.error(logger, this, signature, new IllegalArgumentException("The argument " + name
                + " cannot be null."));
        }
    }

    /**
     * <p>
     * Throw IllegalArgumentException if the parameter is null or empty.
     * </p>
     *
     * @param signature The signature of the method to be logged.
     * @param param the argument to be checked
     * @param name the name of the argument showed in exception
     * @throws IllegalArgumentException If the argument does not meet the request.
     */
    private void checkStringNotNullAndEmpty(String signature, String param, String name) {
        checkNotNull(signature, param, name);
        if (param.trim().length() == 0) {
            throw LogUtil.error(logger, this, signature, new IllegalArgumentException("The argument " + name
                + " cannot be empty."));
        }
    }
}
